package com.behelpful.demo.gateway.filter;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.behelpful.common.core.constants.OAuth2Parameter;
import com.behelpful.common.core.constants.SecurityConstant;
import com.behelpful.common.core.enums.AuthValidateTypeEnum;
import com.behelpful.common.core.exceptions.IAuthenticationException;
import com.behelpful.common.core.exceptions.ValidateCodeException;
import com.behelpful.common.core.result.ErrorResult;
import com.behelpful.demo.gateway.config.FilterIgnoreProperties;
import com.behelpful.demo.gateway.entity.ValidateCodePo;
import com.behelpful.common.integration.iredis.constants.ValidateCodeEnum;
import com.behelpful.common.integration.iredis.support.IRedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import cn.hutool.core.codec.Base64;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 授权验证过滤器
 *
 * @author Alay
 * @date 2021-09-24 11:33
 * 参考 {博客 https://www.jb51.net/article/201115.htm}
 * @see org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
 */
@Slf4j
@Component
public class AuthValidateGatewayFilterFactory extends AbstractGatewayFilterFactory {

    @Autowired
    private FilterIgnoreProperties ignoreProperties;
    @Autowired
    private IRedisUtil iRedisUtil;

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public GatewayFilter apply(Object config) {
        return (ServerWebExchange exchange, GatewayFilterChain chain) -> {

            ServerHttpRequest request = exchange.getRequest();

            // 判断是否是登录授权请求,认证、登录授权只接受 POST 请求
            HttpMethod method = request.getMethod();
            if (HttpMethod.POST != method) {
                // 直接跳过
                return chain.filter(exchange);
            }
            String url = request.getURI().getPath();
            if (!StrUtil.containsAny(url, SecurityConstant.OAUTH_TOKEN_URL, SecurityConstant.USERNAME_LOGIN_URL)) {
                // 不是登录,授权的接口
                return chain.filter(exchange);
            }

            // 忽略校验的 client_id
            String clientIdHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            String[] clientInfoArr = this.resolveToken(clientIdHeader);
            String client_id = clientInfoArr[0];
            String client_secret = clientInfoArr[1];

            // client_id 判断是否需要验证
            if (StrUtil.isNotBlank(client_id) && ignoreProperties.getClients().contains(client_id)) {
                // 忽略检验的客户端直接放行,执行下一步
                return chain.filter(exchange);
            }

            ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
            Mono modifiedBody = serverRequest.bodyToMono(String.class)
                    .flatMap(bodyStr -> {
                        Map<String, Object> requestBodyMap = this.decodeBody(bodyStr);

                        requestBodyMap.put(OAuth2Parameter.CLIENT_ID, client_id);
                        requestBodyMap.put(OAuth2Parameter.CLIENT_SECRET, client_secret);

                        // 修改新的请求体
                        Mono<String> newReturn = Mono.just(this.encodeBody(requestBodyMap));

                        // 授权类型判断处理
                        String grantType = MapUtil.getStr(requestBodyMap, OAuth2Parameter.GRANT_TYPE);
                        if (OAuth2Parameter.REFRESH_TOKEN.equals(grantType)) {
                            // 刷新token,向下转发
                            return newReturn;
                        }

                        // 验证方式
                        Integer typeCode = MapUtil.getInt(requestBodyMap, OAuth2Parameter.VALIDATE_TYPE);
                        // 验证码 id
                        String codeId = MapUtil.getStr(requestBodyMap, "codeId");
                        // 验证码
                        String code = MapUtil.getStr(requestBodyMap, "code");
                        ValidateCodePo validateCodePo = ValidateCodePo.builder().code(code).codeId(codeId).validateType(typeCode);
                        // 执行验证码验证
                        this.doValidateCode(validateCodePo);

                        return newReturn;
                    });

            // 构建新的请求体
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);

            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
            return bodyInserter.insert(outputMessage, new BodyInserterContext())
                    .then(Mono.defer(() -> {
                        ServerHttpRequest decorator = decorate(exchange, headers, outputMessage);
                        return chain.filter(exchange.mutate().request(decorator).build());
                    }));
        };
    }

    private ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers,
                                                CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(headers);
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    // TODO: this causes a 'HTTP/1.1 411 Length Required' // on
                    // httpbin.org
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }


    /**
     * 请求头解析 ClientId
     *
     * @param clientIdHeader
     * @return
     */
    private String[] resolveToken(String clientIdHeader) {
        if (null == clientIdHeader) {
            throw new IAuthenticationException(ErrorResult.ILLEGAL_REQUEST);
        }
        // Basic
        String tokenBase64 = clientIdHeader.substring(SecurityConstant.AUTHORIZATION_BASIC.length());

        String token = this.base64Decode(tokenBase64);
        if (StrUtil.isBlank(token)) {
            throw new IAuthenticationException(ErrorResult.ILLEGAL_REQUEST);
        }
        String[] tokenArr = token.split(StrUtil.COLON);
        return tokenArr;
    }

    /**
     * Base64 解码
     *
     * @param base64Str
     * @return
     */
    private String base64Decode(String base64Str) {
        byte[] base64Bytes = base64Str.getBytes();
        byte[] base64Decode = Base64.decode(base64Bytes);
        String result = new String(base64Decode, StandardCharsets.UTF_8);
        return result;
    }


    /**
     * 执行验证码验证（若验证方式较多可将验证方式抽取类策略处理）
     *
     * @param validateCodePo
     */
    private void doValidateCode(ValidateCodePo validateCodePo) {
        if (null == validateCodePo.codeId() || null == validateCodePo.code() || null == validateCodePo.validateType()) {
            throw new ValidateCodeException(ErrorResult.VALIDATE_TYPE_NONSUPPORT);
        }
        AuthValidateTypeEnum validateType = AuthValidateTypeEnum.codeOf(validateCodePo.validateType());
        if (StrUtil.isBlank(validateCodePo.code())) {
            // 验证码为空
            throw new ValidateCodeException(ErrorResult.VALIDATE_CODE_EMPTY);
        }
        switch (validateType) {
            case ARITHMETIC_CAPTCHA:
                this.arithmeticCaptcha(validateCodePo.codeId(), validateCodePo.code());
                break;
            default:
                throw new ValidateCodeException(ErrorResult.VALIDATE_TYPE_NONSUPPORT);
        }
    }


    /**
     * 算数验证码验证
     *
     * @param codeId
     * @param code
     */
    private void arithmeticCaptcha(String codeId, String code) {
        boolean isPass = iRedisUtil.verifyValidateCode(ValidateCodeEnum.LOGIN_VALIDATE, codeId, code);
        if (!isPass) {
            // 验证码不合法
            throw new ValidateCodeException(ErrorResult.VALIDATE_CODE_ERR);
        }
    }


    /**
     * 参数解析为 Map<String,String>
     * grant_type=password&username=admin&password=admin&client_id=admin&client_secret=behelpful_admin&code=12&validateType=1&codeId=1441352619698106368
     *
     * @param bodyStr
     * @return
     */
    private Map<String, Object> decodeBody(String bodyStr) {
        Map<String, Object> result = new HashMap<>();
        String[] split = bodyStr.split("&");
        Arrays.stream(split).forEach(item -> {
            String[] k_v = item.split("=");
            result.put(k_v[0], k_v[1]);
        });
        return result;
    }

    private String encodeBody(Map<String, Object> map) {
        return map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
    }

    /**
     * 测试生成 base64码
     */
    private void genCode() {
        String clientId = "admin";
        String clientSecret = "behelpful_admin";

        String token = clientId + StrUtil.COLON + clientSecret;

        String tokenBase64 = Base64.encode(token);
        // 编码结果
        String Authorization = SecurityConstant.AUTHORIZATION_BASIC + tokenBase64;
        log.warn(">>>>  client_id 编码结果: {}", Authorization);
    }

}